<?PHP
#
#   FILE:  Page.php (Pages plugin)
#
#   Copyright 2012-2022 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#
# @scout:phpstan

namespace Metavus\Plugins\Pages;

use Metavus\Image;
use Metavus\ImageFactory;
use Metavus\MetadataField;
use Metavus\MetadataSchema;
use Metavus\PrivilegeSet;
use Metavus\Record;
use ScoutLib\PluginManager;
use ScoutLib\StdLib;
use ScoutLib\User;

/**
 * Class representing individual pages in the Pages plugin.
 */
class Page extends Record
{
    # ---- PUBLIC INTERFACE --------------------------------------------------

    /**
     * Object constructor for loading an existing page.  (To create a new
     * page, use Page::Create().).
     * @param int $PageId ID of the page to load.
     */
    public function __construct($PageId)
    {
        # run base class constructor
        parent::__construct($PageId);

        # load viewing privilege setting if available
        $Data = $this->DB->queryValue(
            "SELECT ViewingPrivileges"
                ." FROM Pages_Privileges"
                ." WHERE PageId = ".intval($PageId),
            "ViewingPrivileges"
        );
        $this->ViewingPrivs = ($Data === false) ? new PrivilegeSet()
                : new PrivilegeSet($Data);
    }

    /**
     * Create a new page.
     * @param int $SchemaId XX (DO NOT USE -- for compatibility with Resource::Create())
     * @return Page Page object.
     * @throws \Exception If bad metadata schema ID parameter is provided.
     */
    public static function create(int $SchemaId = null): Page
    {
        # bail out of erroneous schema ID was provided
        if (($SchemaId !== null) && ($SchemaId != PageFactory::$PageSchemaId)) {
            throw new \Exception("Unnecessary and erroneous schema ID provided.");
        }

        # create a resource
        $Resource = Record::create(PageFactory::$PageSchemaId);

        # reload resource as a page
        $Id = $Resource->Id();
        unset($Resource);
        $Page = new Page($Id);

        # return page to caller
        return $Page;
    }

    /**
     * Set value using field name, ID, or field object.
     * @param int|string|MetadataField $Field Field ID or full name of field
     *      or MetadataField object.
     * @param mixed $NewValue New value for field.
     * @param bool $Reset When TRUE Controlled Names, Classifications,
     *       and Options will be set to contain *ONLY* the contents of
     *       NewValue, rather than appending $NewValue to the current value.
     * @throws \Exception When attempting to set a value for a field that is
     *       part of a different schema than the resource.
     * @throws \InvalidArgumentException When attempting to set a controlled
     *       name with an invalid ID.
     * @see Record::set().
     */
    public function set($Field, $NewValue, bool $Reset = false)
    {
        $Field = $this->normalizeFieldArgument($Field);

        # return if we don't have a valid field
        if (!($Field instanceof MetadataField)) {
            return;
        }

        # handle image keyword replacement
        if ($Field->type() == MetadataSchema::MDFTYPE_PARAGRAPH) {
            $NewValue = ImageFactory::convertUrlsToKeywords($NewValue);
        }

        parent::set($Field, $NewValue, $Reset);

        if ($Field->name() == "Clean URL") {
            PluginManager::getInstance()
                ->getPlugin("Pages", true)
                ->clearCaches();
        }
    }

    /**
     * Remove page (and accompanying associations) from database and
     * delete any associated files.
     */
    public function destroy()
    {
        $this->DB->query("DELETE FROM Pages_Privileges WHERE PageId = ".$this->id());

        parent::destroy();
        PluginManager::getInstance()
            ->getPlugin("Pages", true)
            ->clearCaches();
    }

    /**
     * Get a value from a Page. Expands image keywords in the Paragraph fields to
     *   URLs.
     * @param int|string|MetadataField $Field Field ID or full name of field
     *      or MetadataField object.
     * @param bool $ReturnObject For field types that can return multiple values, if
     *      TRUE, returns array of objects, else returns array of values.
     *      Defaults to FALSE.
     * @param bool $IncludeVariants If TRUE, includes variants in return value.
     *      Only applicable for ControlledName fields.
     * @return mixed Requested object(s) or value(s).  Returns empty array
     *      (for field types that allow multiple values) or NULL (for field
     *      types that do not allow multiple values) if no values found.  Returns
     *      NULL if field does not exist or was otherwise invalid.
     * @see Record::get()
     */
    public function get($Field, bool $ReturnObject = false, bool $IncludeVariants = false)
    {
        $Field = $this->normalizeFieldArgument($Field);
        $Value = parent::get($Field, $ReturnObject, $IncludeVariants);

        if ($Field->type() == MetadataSchema::MDFTYPE_PARAGRAPH) {
            $Value = (new ImageFactory())->convertKeywordsToUrls((string)$Value);
        }

        return $Value;
    }

    /**
     * Determine if the given user can view the page.  The result of this
     * method may be modified via EVENT_RESOURCE_VIEW_PERMISSION_CHECK.
     * @param User $User User to check.
     * @param bool $AllowHooksToModify If TRUE, hooks to the modification
     *       event will be allowed to change the result.
     * @return bool TRUE if the user can view the page and FALSE otherwise
     */
    public function userCanView(User $User, bool $AllowHooksToModify = true): bool
    {
        # construct a key to use for our permissions cache
        $CacheKey = "UserCanView".$User->id();

        # if we don't have a cached value for this perm, compute one
        if (!isset($this->PermissionCache[$CacheKey])) {
            # check passes if user privileges are greater than resource set
            $CheckResult = ($this->ViewingPrivs->MeetsRequirements($User, $this)
                    && parent::userCanView($User, false)) ? true : false;

            # save the result of this check in our cache
            $this->PermissionCache[$CacheKey] = $CheckResult;
        }

        # retrieve value from cache
        $Value = $this->PermissionCache[$CacheKey];

        # allow any hooked functions to modify the result if allowed
        if ($AllowHooksToModify) {
            $SignalResult = $GLOBALS["AF"]->SignalEvent(
                "EVENT_RESOURCE_VIEW_PERMISSION_CHECK",
                [
                    "Resource" => $this,
                    "User" => $User,
                    "CanView" => $Value,
                    "Schema" => $this->getSchema(),
                ]
            );
            $Value =  $SignalResult["CanView"];
        }

        # return result to caller
        return $Value;
    }

    /**
     * Get/set viewing privileges for page.
     * @param PrivilegeSet $NewValue New viewing privilege setting.  (OPTIONAL)
     * @return PrivilegeSet|null Current viewing privilege setting or NULL if no
     *       viewing privileges have been set.
     */
    public function viewingPrivileges(PrivilegeSet $NewValue = null)
    {
        # if new privilege setting was supplied
        if ($NewValue !== null) {
            # save new setting in database
            $Value = $this->DB->queryValue(
                "SELECT ViewingPrivileges"
                    ." FROM Pages_Privileges"
                    ." WHERE PageId = ".intval($this->id()),
                "ViewingPrivileges"
            );
            if ($this->DB->NumRowsSelected()) {
                $this->DB->Query("UPDATE Pages_Privileges"
                        ." SET ViewingPrivileges ="
                        ." '".addslashes($NewValue->Data())."'"
                        ." WHERE PageId = ".intval($this->id()));
            } else {
                $this->DB->Query("INSERT INTO Pages_Privileges"
                        ." (PageId, ViewingPrivileges) VALUES"
                        ." (".intval($this->id()).","
                        ." '".addslashes($NewValue->Data())."')");
            }

            # clear cached permissions
            $this->clearPermissionsCache();

            # save new setting for local use
            $this->ViewingPrivs = $NewValue;
        } else {
            # if setting has not already been loaded
            if (!isset($this->ViewingPrivs)) {
                # load setting from database
                $Value = $this->DB->queryValue(
                    "SELECT ViewingPrivileges"
                        ." FROM Pages_Privileges"
                        ." WHERE PageId = ".intval($this->id()),
                    "ViewingPrivileges"
                );
                $this->ViewingPrivs = ($Value === false) ? null
                        : new PrivilegeSet($Value);
            }
        }

        # return current privilege settings to caller
        return $this->ViewingPrivs;
    }

    /**
     * Check whether current page contains jquery-ui tabs.
     * @return bool TRUE for pages that have tabs, FALSE otherwise.
     */
    public function containsTabs(): bool
    {
        return count($this->getTabNames()) > 0;
    }

    /**
     * Get the names of jquery-ui tabs on the page, where the names are the
     * down-cased alphanumeric portions of the text in the tab header (e.g.,
     * for a tab with a header that said "Student Success" the tab name would
     * be "studentsuccess").
     * @return array of tab names; empty for pages that have no tabs.
     */
    public function getTabNames(): array
    {
        if (!is_null($this->TabNames)) {
            return $this->TabNames;
        }

        preg_match_all(
            '%<h2 class="mv-tab-start">(.*)</h2>%',
            $this->get("Content"),
            $Matches,
            PREG_PATTERN_ORDER
        );

        $this->TabNames = [];

        foreach ($Matches[1] as $TabTitle) {
            $this->TabNames[] = strtolower(
                preg_replace(
                    "/[^A-Za-z0-9]/",
                    "",
                    strip_tags($TabTitle)
                )
            );
        }

        return $this->TabNames;
    }

    /**
     * Convert the Pages internal markup for tabs into the format that jquery-ui wants
     * @param string $Html Html to process
     * @return string processed HTML.
     */
    public static function processTabMarkup(string $Html): string
    {
        # divide the input up into pretab, tab header, and tab content
        $PretabContent = '';
        $TabHeader = '';
        $TabContent = '';
        $ActiveTabParameter = '';

        # keep track of which tab we are in, zero if we aren't in one yet
        $Count = 0;

        # iterate through all the lines (assumes ckeditor will
        #   keep the <h2's on their own line)
        foreach (explode("\n", $Html) as $Line) {
            # if this is the beginning of a new tab
            if (preg_match('%<h2 class="mv-tab-start">(.*)</h2>%', $Line, $Matches)) {
                $Count++;

                # when we're the 2nd or later tab, end the previous one
                if ($Count > 1) {
                    $TabContent .= "</div>\n";
                }

                # start a new tab and add it to the header
                $TabHeader .= "<li><a href='#mv-tabs-".$Count."'>".
                           $Matches[1]."</a></li>";
                $TabContent .= "<div id='mv-tabs-".$Count."' class='clearfix'>\n";

                # look for and save active tab setting
                if (isset($_GET["AT"])) {
                    $TabKeyword = strtolower(preg_replace(
                        "/[^A-Za-z0-9]/",
                        "",
                        strip_tags($Matches[1])
                    ));
                    if (strtolower($_GET["AT"]) == $TabKeyword) {
                        $ActiveTabParameter = "{active: ".($Count - 1)."}";
                    }
                }
            } else {
                # if we're not in a tab yet, this line is pretab content
                #  otherwise, it is tab content
                if ($Count == 0) {
                    $PretabContent .= $Line;
                } else {
                    $TabContent .= $Line;
                }
            }
        }
        # end the last tab
        $TabContent .= "</div>\n";

        # generate the markup and the javascript for jquery-ui to make our tabs
        return $PretabContent
            ."<div class='cw-pages-float-barrier'></div>"
            ."<div id='mv-tabs'>"
            ."<ul>".$TabHeader."</ul>"
            .$TabContent
            ."</div>"
            ."<script type='text/javascript'>"
            ."jQuery(document).ready(function() { jQuery('#mv-tabs').tabs("
                    .$ActiveTabParameter.");});"
            ."</script>";
    }

    /**
     * Get page summary text, based on page content.
     * @param int $Length Target length for summary, in characters.  (OPTIONAL,
     *       defaults to 320)
     * @return string Summary text.
     */
    public function getSummary(int $Length = 280): string
    {
        # get main page content
        $Content = $this->get("Content");

        # strip out any headings
        $Content = preg_replace("%<h[1-3]>.+</h[1-3]>%", "", $Content);

        # strip out any HTML
        $Content = strip_tags($Content);

        # strip any extraneous whitespace
        $Content = preg_replace("%\s+%", " ", trim($Content));

        # truncate result to desired length
        $Summary = StdLib::neatlyTruncateString($Content, $Length);

        # return "summary" to caller
        return $Summary;
    }

    /**
     *  Get/set whether Page is a temporary record.
     * @param bool $NewSetting TRUE/FALSE setting for whether resource is
     *       temporary. (OPTIONAL)
     * @return bool TRUE if resource is temporary record, or FALSE otherwise.
     */
    public function isTempRecord(bool $NewSetting = null): bool
    {
        $OldId = $this->id();
        $Result = parent::isTempRecord($NewSetting);
        $NewId = $this->id();
        if ($NewId != $OldId) {
            $this->DB->query("UPDATE Pages_Privileges SET PageId = "
                .$NewId." WHERE PageId = ".$OldId);
        }
        return $Result;
    }


    # ---- PRIVATE INTERFACE -------------------------------------------------

    private $TabNames = null;
    private $ViewingPrivs;
}
